This project is part of the
@thi.ng/umbrella monorepo and anti-framework.
About
HTML/SVG/XML serialization of nested data structures, iterables & closures.
Inspired by Hiccup and
Reagent for Clojure/ClojureScript.
Forget all the custom toy DSLs for templating and instead use the full
power of ES6 to directly define fully data-driven, purely functional and
easily composable components for static serialization to HTML &
friends.
This library is suitable for static website generation, server side rendering
etc. For interactive use cases, please see companion packages
@thi.ng/rdom
(or the older
@thi.ng/hdom)
and their various support packages.
Features
- Only uses arrays, functions, ES6 iterables / iterators / generators
- Eager & lazy component composition using embedded functions / closures
- Support for self-closing tags (incl. validation), boolean attributes
- Arbitrary user context object injection for component functions
- Dynamic derived attribute value generation via function values
- CSS formatting of
style
attribute objects - Optional HTML entity encoding
- Support for comments and XML/DTD processing instructions
- Branch-local behavior control attributes to control serialization
- Small (1.9KB minified) & fast
(*) Lazy composition here means that functions are only executed at
serialization time. Examples below...
Use cases
- Serverside rendering
- Static site, feed generation
.innerHTML
body generation- SVG asset creation
- Shape trees for declarative canvas API drawing
- Generic intermediate representation format for many other use cases...
No special sauce needed (or wanted)
Using only vanilla language features simplifies the development,
composability, reusability and testing of components. Furthermore, no
custom template parser is required and you're only restricted by the
expressiveness of the language / environment, not by your template
engine.
Components can be defined as simple functions returning arrays or loaded
via JSON/JSONP.
What is Hiccup?
For many years, Hiccup has been
the de-facto standard to encode HTML/XML datastructures in Clojure. This
library brings & extends this convention into ES6. A valid Hiccup tree
is any flat (though, usually nested) array of the following possible
structures. Any functions embedded in the tree are expected to return
values of the same structure. Please see examples &
API further explanations...
["tag", ...]
["tag#id.class1.class2", ...]
["tag", {other: "attrib", ...}, ...]
["tag", {...}, "body", 23, function, [...]]
[function, arg1, arg2, ...]
[{render: (ctx, ...args) => [...]}, args...]
iterable
Status
STABLE - used in production
Search or submit any issues for this package
Support packages
Related packages
Blog posts
Installation
yarn add @thi.ng/hiccup
ES module import:
<script type="module" src="https://cdn.skypack.dev/@thi.ng/hiccup"></script>
Skypack documentation
For Node.js REPL:
const hiccup = await import("@thi.ng/hiccup");
Package sizes (brotli'd, pre-treeshake): ESM: 2.09 KB
Dependencies
Usage examples
Several demos in this repo's
/examples
directory are using this package.
A selection:
Screenshot | Description | Live demo | Source |
---|
| Heatmap visualization of this mono-repo's commits | | Source |
| Filterable commit log UI w/ minimal server to provide commit history | Demo | Source |
| Various hdom-canvas shape drawing examples & SVG conversion / export | Demo | Source |
| Generating pure CSS image transitions | Demo | Source |
| Hiccup / hdom DOM hydration example | Demo | Source |
| Markdown to Hiccup to HTML parser / transformer | Demo | Source |
| CLI util to visualize umbrella pkg stats | | Source |
| Generate SVG using pointfree DSL | | Source |
| Interactive grid generator, SVG generation & export, undo/redo support | Demo | Source |
API
Generated API docs
Tags with Zencoding expansion
Tag names support
Zencoding/Emmet
style ID & class attribute expansion:
serialize(
["div#yo.hello.world", "Look ma, ", ["strong", "no magic!"]]
);
<div id="yo" class="hello world">Look ma, <strong>no magic!</strong></div>
Attributes
Arbitrary attributes can be supplied via an optional 2nd array element.
style
attributes can be given as CSS string or as an object. Boolean
attributes are serialized in HTML5 syntax (i.e. present or not, but no
values).
If the 2nd array element is not a plain object, it's treated as normal
child node (see previous example).
serialize(
["div.notice",
{
selected: true,
style: {
background: "#ff0",
border: "3px solid black"
}
},
"WARNING"]
);
<div class="notice" selected style="background:#ff0;border:3px solid black">WARNING</div>
If an attribute specifies a function as value, the function is called
with the entire attribute object as argument. This allows for the
dynamic generation of attribute values, based on existing ones. The
result MUST be a string.
Function values for event attributes (any attrib name starting with
"on") WILL BE OMITTED from output.
["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
<div id="foo" bar="foo-bar"></div>
["div#foo", { onclick: () => alert("foo") }, "click me!"]
<div id="foo">click me!</div>
["div#foo", { onclick: "alert('foo')" }, "click me!"]
<div id="foo" onclick="alert('foo')">click me!</div>
Simple components
const thumb = (src) => ["img.thumb", { src, alt: "thumbnail" }];
serialize(
["div.gallery", ["foo.jpg", "bar.jpg", "baz.jpg"].map(thumb)]
);
<div class="gallery">
<img class="thumb" src="foo.jpg" alt="thumbnail"/>
<img class="thumb" src="bar.jpg" alt="thumbnail"/>
<img class="thumb" src="baz.jpg" alt="thumbnail"/>
</div>
User context injection
Every component function will receive an arbitrary user defined context
object as first argument. This context object is passed to serialize()
and is then auto-injected for every component function call.
The context object should contain any global component configuration,
e.g. for theming purposes.
const header = (ctx, body) =>
["h1", ctx.theme.title, body];
const section = (ctx, title, ...body) =>
["section", ctx.theme.section, [header, title], ...body];
const theme = {
section: { class: "bg-black moon-gray bt b--dark-gray mt3" },
title: { class: "white f3" }
};
serialize(
[section, "Hello world", "Easy theming"],
{ theme }
);
Note: Of course the context is ONLY auto-injected for lazily
embedded component functions (as shown above), i.e. if the functions are
wrapped in arrays and only called during serialization. If you call a
component function directly, you MUST pass the context (or null
) as
first arg yourself. Likewise, if a component function doesn't make use
of the context you can either:
const div = (attribs, body) => ["div", attribs, body];
serialize(div({id: "foo"}, "bar"));
Or...
const div = (_, attribs, body) => ["div", attribs, body];
serialize(div(null, {id: "foo"}, "bar"));
serialize([div, {id: "foo"}, "bar"]);
SVG generation, generators & lazy composition
Also see
@thi.ng/hiccup-svg
for related functionality.
const fs = require("fs");
const circle = (_, x, y, r) => ["circle", { cx: x | 0, cy: y | 0, r: r | 0 }];
const randomCircle = () => [
circle,
Math.random() * 1000,
Math.random() * 1000,
Math.random() * 100
];
function* repeatedly(n, fn) {
while (n-- > 0) yield fn();
}
import { SVG_NS } from "@thi.ng/hiccup";
const doc = [
"svg", { xmlns: SVG_NS, width: 1000, height: 1000 },
["g", { fill: "none", stroke: "red" },
repeatedly(100, randomCircle)]];
fs.writeFileSync("circles.svg", serialize(doc));
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000">
<g fill="none" stroke="red">
<circle cx="182" cy="851" r="66"/>
<circle cx="909" cy="705" r="85"/>
<circle cx="542" cy="915" r="7"/>
<circle cx="306" cy="762" r="88"/>
...
</g>
</svg>
Data-driven component composition
const glossary = {
foo: "widely used placeholder name in computing",
bar: "usually appears in combination with 'foo'",
hiccup: "de-facto standard format to define HTML in Clojure",
toxi: "author of this fine library",
};
const dlItem = (index, key) => [["dt", key], ["dd", index[key]]];
const objectList = (f, items) => Object.keys(items).sort().map((k)=> f(items, k));
const dlList = (_, attribs, items) => ["dl", attribs, objectList(dlItem, items)];
const widget = [
"div.widget",
["h1", "Glossary"],
[dlList, { id: "glossary" }, glossary]];
serialize(widget, null, true);
<div class="widget">
<h1>Glossary</h1>
<dl id="glossary">
<dt>bar</dt>
<dd>usually appears in combination with 'foo'</dd>
<dt>foo</dt>
<dd>widely used placeholder name in computing</dd>
<dt>hiccup</dt>
<dd>de-facto standard format to define HTML in Clojure</dd>
<dt>toxi</dt>
<dd>author of this fine library</dd>
</dl>
</div>
Stateful component
const indexer = (prefix = "sec") => {
let counts = new Array(6).fill(0);
return (_, level, title) => {
counts[level - 1]++;
counts.fill(0, level);
return [
["a", { name: "sec-" + counts.slice(0, level).join(".") }],
["h" + level, title]
];
};
};
const TOC = [
[1, "Document title"],
[2, "Preface"],
[3, "Thanks"],
[3, "No thanks"],
[2, "Chapter"],
[3, "Exercises"],
[4, "Solutions"],
[2, "The End"]
];
const section = indexer();
serialize([
"div.toc",
TOC.map(([level, title]) => [section, level, title])
]);
<div class="toc">
<a name="sec-1"></a><h1>Document title</h1>
<a name="sec-1.1"></a><h2>Preface</h2>
<a name="sec-1.1.1"></a><h3>Thanks</h3>
<a name="sec-1.1.2"></a><h3>No thanks</h3>
<a name="sec-1.2"></a><h2>Chapter</h2>
<a name="sec-1.2.1"></a><h3>Exercises</h3>
<a name="sec-1.2.1.1"></a><h4>Solutions</h4>
<a name="sec-1.3"></a><h2>The End</h2>
</div>
Component objects
The sibling library
@thi.ng/hdom
supports components with basic life cycle methods (init, render,
release). In order to support serialization of hdom component trees,
hiccup too supports such components since version 2.0.0. However, for
static serialization only the render
method is of interest and others
are ignored.
const component = {
render: (ctx, title, ...body) => ["section", ["h1", title], ...body]
};
serialize([component, "Hello world", "Body"]);
Behavior control attributes
The following attributes can be used to control the serialization
behavior of individual elements / tree branches:
__skip
- if true, skips serialization (also used by
@thi.ng/hdom)__serialize
- if false, skips serialization (hiccup only)
serialize(["div.container", ["div", {__skip: true}, "ignore me"]]);
Single or multiline comments can be included using the special COMMENT
tag (__COMMENT__
) (always WITHOUT attributes!).
[COMMENT, "Hello world"]
[COMMENT, "Hello", "world"]
XML / DTD processing instructions
Currently, the only processing / DTD instructions supported are:
?xml
!DOCTYTPE
!ELEMENT
!ENTITY
!ATTLIST
These are used as follows (attribs are only allowed for ?xml
, all
others only accept a body string which is taken as is):
["?xml", { version: "1.0", standalone: "yes" }]
["!DOCTYPE", "html"]
Emitted processing instructions are always succeeded by a newline
character.
API
The library exposes these two functions:
serialize()
Signature: serialize(tree: any, ctx?: any, escape = false): string
Recursively normalizes and serializes given tree as HTML/SVG/XML string.
Expands any embedded component functions with their results. Each node
of the input tree can have one of the following input forms:
["tag", ...]
["tag#id.class1.class2", ...]
["tag", {other: "attrib"}, ...]
["tag", {...}, "body", function, ...]
[function, arg1, arg2, ...]
[{render: (ctx,...) => [...]}, args...]
iterable
Tags can be defined in "Zencoding" convention, e.g.
["div#foo.bar.baz", "hi"]
The presence of the attributes object (2nd array index) is optional. Any
attribute values, incl. functions are allowed. If the latter, the
function is called with the full attribs object as argument and the
return value is used for the attribute. This allows for the dynamic
creation of attrib values based on other attribs. The only exception to
this are event attributes, i.e. attribute names starting with "on".
["div#foo", { bar: (attribs) => attribs.id + "-bar" }]
The style
attribute can ONLY be defined as string or object.
["div", {style: {color: "red", background: "#000"}}]
Boolean attribs are serialized in HTML5 syntax (present or not). null or
empty string attrib values are ignored.
Any null
or undefined
array values (other than in head position) will be
removed, unless a function is in head position.
A function in head position of a node acts as a mechanism for component
composition & delayed execution. The function will only be executed at
serialization time. In this case the optional global context object and
all other elements of that node / array are passed as arguments when
that function is called. The return value the function MUST be a valid
new tree (or undefined).
const foo = (ctx, a, b) => ["div#" + a, ctx.foo, b];
serialize([foo, "id", "body"], { foo: { class: "black" }})
Functions located in other positions are called ONLY with the global
context arg and can return any (serializable) value (i.e. new trees,
strings, numbers, iterables or any type with a suitable .toString()
implementation).
Please also see list of supported behavior control
attributes.
escape()
Signature: escape(str: string): string
Helper function. Applies HTML entity replacement on given string. If
serialize()
is called with true
as 2nd argument, entity encoding is
done automatically (list of entities
considered).
Authors
If this project contributes to an academic publication, please cite it as:
@misc{thing-hiccup,
title = "@thi.ng/hiccup",
author = "Karsten Schmidt",
note = "https://thi.ng/hiccup",
year = 2016
}
License
© 2016 - 2023 Karsten Schmidt // Apache License 2.0